#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "../game/q_shared.h"
#include "../qcommon/qcommon.h"

typedef struct zfCentralEntry_s
{
	unsigned long sig;

	unsigned short versionMade;
	unsigned short versionExtract;

	unsigned short bitFlag;
	unsigned short method;

	unsigned short modTime;
	unsigned short modDate;

	unsigned long crc32;

	unsigned long compressedSize;
	unsigned long uncompressedSize;

	unsigned short fileNameLen;
	unsigned short extraFieldLen;
	unsigned short fileCommentLen;
	unsigned short diskNumberStart;

	unsigned short internalAttributes;
	unsigned long  externalAttributes;

	unsigned long localHeaderOffset;

	char *fileName;

	struct zfCentralEntry_s *oldEntry;
} zfCentralEntry_t;

typedef struct
{
	FILE *f;
	long numZipFiles;
	zfCentralEntry_t *zipFiles;
} zfZipArchive_t;

zfZipArchive_t oldArchive;
zfZipArchive_t newArchive;
FILE *outZip;
long expectedSize;



static void ReadULong(FILE *f, unsigned long *l) { fread(l, 4, 1, f); }
static void ReadLong(FILE *f, long *l) { fread(l, 4, 1, f); }

static void ReadUShort(FILE *f, unsigned short *s) { fread(s, 2, 1, f); }
static void ReadShort(FILE *f, short *s) { fread(s, 2, 1, f); }

static void ReadString(FILE *f, char **pstr, unsigned short len)
{
	char *str;

	str = malloc(len+1);
	if(len) fread(str, len, 1, f);
	str[len] = '\0';
	*pstr = str;
}


static void WriteULong(FILE *f, unsigned long l) { fwrite(&l, 4, 1, f); }
static void WriteLong(FILE *f, long l) { fwrite(&l, 4, 1, f); }

static void WriteUShort(FILE *f, unsigned short s) { fwrite(&s, 2, 1, f); }
static void WriteShort(FILE *f, short s) { fwrite(&s, 2, 1, f); }

static void WriteString(FILE *f, const char *c, unsigned short len)
{
	if(len) fwrite(c, len, 1, f);
}




typedef struct
{
	unsigned long  sig;

	unsigned short diskNum;
	unsigned short diskCentralDir;
	unsigned short centralDirThisDisk;
	unsigned short centralDirTotal;
	unsigned long  centralDirSize;
	unsigned long  centralDirOffset;
	unsigned short zipCommentLen;
} zfCentralDirEnd_t;

static zfCentralDirEnd_t dirEnding;

static void ReadCentralDirEnd(FILE *f, zfCentralDirEnd_t *s)
{
	// 0x04034b50
	ReadULong(f, &s->sig);
	ReadUShort(f, &s->diskNum);
	ReadUShort(f, &s->diskCentralDir);
	ReadUShort(f, &s->centralDirThisDisk);
	ReadUShort(f, &s->centralDirTotal);
	ReadULong(f, &s->centralDirSize);
	ReadULong(f, &s->centralDirOffset);
	ReadUShort(f, &s->zipCommentLen);
}

static void WriteCentralDirEnd(FILE *f, zfCentralDirEnd_t *s)
{
	WriteULong(f, 0x06054b50);
	WriteUShort(f, 0);
	WriteUShort(f, 0);
	WriteUShort(f, s->centralDirTotal);
	WriteUShort(f, s->centralDirTotal);
	WriteULong(f, s->centralDirSize);
	WriteULong(f, s->centralDirOffset);
	WriteUShort(f, 0);
}



typedef struct
{
	unsigned long sig;

	unsigned short extractVersion;
	unsigned short bitFlag;

	unsigned short compressionMethod;
	unsigned short modTime;
	unsigned short modDate;

	unsigned long crc32;

	unsigned long compressedSize;
	unsigned long uncompressedSize;

	unsigned short fileNameLen;
	unsigned short extraFieldLen;

	char *fileName;
} zfLocalHeader_t;

static char *dummyString;

static void ReadLocalFileHeader(FILE *f, zfLocalHeader_t *s)
{
	// 0x04034b50
	ReadULong(f, &s->sig);

	ReadUShort(f, &s->extractVersion);
	ReadUShort(f, &s->bitFlag);

	ReadUShort(f, &s->compressionMethod);
	ReadUShort(f, &s->modTime);
	ReadUShort(f, &s->modDate);

	ReadULong(f, &s->crc32);

	ReadULong(f, &s->compressedSize);
	ReadULong(f, &s->uncompressedSize);

	ReadUShort(f, &s->fileNameLen);
	ReadUShort(f, &s->extraFieldLen);

	ReadString(f, &s->fileName, s->fileNameLen);
	ReadString(f, &dummyString, s->extraFieldLen);
	free(dummyString);
}

static void WriteLocalFileHeader(FILE *f, zfCentralEntry_t *s)
{
	WriteULong(f, 0x04034b50);

	WriteUShort(f, s->versionExtract);
	WriteUShort(f, s->bitFlag);

	WriteUShort(f, s->method);
	WriteUShort(f, s->modTime);
	WriteUShort(f, s->modDate);

	WriteULong(f, s->crc32);

	WriteULong(f, s->compressedSize);
	WriteULong(f, s->uncompressedSize);

	WriteUShort(f, s->fileNameLen);
	WriteUShort(f, 0);

	WriteString(f, s->fileName, s->fileNameLen);
}




static void ReadCentralEntry(FILE *f, zfCentralEntry_t *s)
{
	ReadULong(f, &s->sig);

	ReadUShort(f, &s->versionMade);
	ReadUShort(f, &s->versionExtract);

	ReadUShort(f, &s->bitFlag);
	ReadUShort(f, &s->method);

	ReadUShort(f, &s->modTime);
	ReadUShort(f, &s->modDate);

	ReadULong(f, &s->crc32);

	ReadULong(f, &s->compressedSize);
	ReadULong(f, &s->uncompressedSize);

	ReadUShort(f, &s->fileNameLen);
	ReadUShort(f, &s->extraFieldLen);
	ReadUShort(f, &s->fileCommentLen);
	ReadUShort(f, &s->diskNumberStart);

	ReadUShort(f, &s->internalAttributes);
	ReadULong(f, &s->externalAttributes);

	ReadULong(f, &s->localHeaderOffset);

	ReadString(f, &s->fileName, s->fileNameLen);
	ReadString(f, &dummyString, s->extraFieldLen);
	free(dummyString);
	ReadString(f, &dummyString, s->fileCommentLen);
	free(dummyString);
}

static void ReadCompactCentralEntry(FILE *f, zfCentralEntry_t *s)
{
	s->sig = 0x02014b50;
	ReadUShort(f, &s->versionMade);
	ReadUShort(f, &s->versionExtract);

	ReadUShort(f, &s->bitFlag);
	ReadUShort(f, &s->method);

	ReadUShort(f, &s->modTime);
	ReadUShort(f, &s->modDate);

	ReadULong(f, &s->crc32);

	ReadULong(f, &s->compressedSize);
	ReadULong(f, &s->uncompressedSize);

	ReadUShort(f, &s->fileNameLen);
	s->extraFieldLen = 0;
	s->fileCommentLen = 0;
	s->diskNumberStart = 0;

	ReadUShort(f, &s->internalAttributes);
	ReadULong(f, &s->externalAttributes);

	ReadString(f, &s->fileName, s->fileNameLen);
}



static void WriteCentralEntry(FILE *f, zfCentralEntry_t *s)
{
	WriteULong(f, s->sig);

	WriteUShort(f, s->versionMade);
	WriteUShort(f, s->versionExtract);

	WriteUShort(f, s->bitFlag);
	WriteUShort(f, s->method);

	WriteUShort(f, s->modTime);
	WriteUShort(f, s->modDate);

	WriteULong(f, s->crc32);

	WriteULong(f, s->compressedSize);
	WriteULong(f, s->uncompressedSize);

	WriteUShort(f, s->fileNameLen);
	WriteUShort(f, 0);
	WriteUShort(f, 0);
	WriteUShort(f, s->diskNumberStart);

	WriteUShort(f, s->internalAttributes);
	WriteULong(f, s->externalAttributes);

	WriteULong(f, s->localHeaderOffset);

	WriteString(f, s->fileName, s->fileNameLen);
}



static int LoadCentralDir(zfZipArchive_t *za)
{
	int i;

	fseek(za->f, -22, SEEK_END);

	ReadCentralDirEnd(za->f, &dirEnding);
	if(dirEnding.sig != 0x06054b50)
	{
		Com_Printf("ZPATCH ERROR: Zip file is either corrupt or has global comment set: %x vs 06054b50\n", dirEnding.sig);
		return 0;
	}

	za->numZipFiles = dirEnding.centralDirTotal;

	fseek(za->f, dirEnding.centralDirOffset, SEEK_SET);

	za->zipFiles = malloc(sizeof(zfCentralEntry_t) * za->numZipFiles);


	for(i=0;i<za->numZipFiles;i++)
	{
		ReadCentralEntry(za->f, za->zipFiles + i);
	}

	return 1;
}




static void CopyData(FILE *sourceFile, zfCentralEntry_t *zf)
{
	char buf[1024];
	unsigned long bytesRemaining;
	zfLocalHeader_t lh;

	bytesRemaining = zf->compressedSize;
	while(bytesRemaining >= 1024)
	{
		fread(buf, 1024, 1, sourceFile);
		fwrite(buf, 1024, 1, outZip);
		bytesRemaining -= 1024;
	}
	if(bytesRemaining)
	{
		fread(buf, bytesRemaining, 1, sourceFile);
		fwrite(buf, bytesRemaining, 1, outZip);
	}
}


static int ApplyPatch(zfZipArchive_t *za)
{
	unsigned long centralDirOffset;
	int i, j, currentSpot, p;
	int matched;
	int newData;
	zfCentralEntry_t *newEntry;
	zfCentralEntry_t *oldEntry;
	zfLocalHeader_t lh;
	zfCentralDirEnd_t dirEnd;
	FILE *sourceFile;

	double currentSpotD;

	p = 0;

	// Get the zip file count
	ReadLong(za->f, &za->numZipFiles);

	za->zipFiles = malloc(sizeof(zfCentralEntry_t) * za->numZipFiles);

	for(i=0;i<za->numZipFiles;i++)
	{
		newEntry = za->zipFiles + i;
		ReadCompactCentralEntry(za->f, newEntry);
		newData = fgetc(za->f);

		if(za->zipFiles[i].compressedSize)
		{
			if(!newData && za->zipFiles[i].compressedSize)
			{
				// Find it in the old archive
				matched = 0;
				for(j=0;j<oldArchive.numZipFiles;j++)
				{
					oldEntry = oldArchive.zipFiles + j;
					if(oldEntry->fileNameLen == newEntry->fileNameLen &&
						oldEntry->compressedSize == newEntry->compressedSize &&
						!memcmp(oldEntry->fileName, newEntry->fileName, newEntry->fileNameLen))
					{
						matched = 1;
						fseek(oldArchive.f, oldEntry->localHeaderOffset, SEEK_SET);
						break;
					}
				}

				if(!matched)
				{
					Com_Printf("ZPATCH error: %s not found in old archive\n", newEntry->fileName);
					return 0;
				}

				sourceFile = oldArchive.f;
				ReadLocalFileHeader(oldArchive.f, &lh);
				free(lh.fileName);
			}
			else
				sourceFile = newArchive.f;
		}
		else
			sourceFile = NULL;

		// Write the local header
		newEntry->localHeaderOffset = ftell(outZip);
		WriteLocalFileHeader(outZip, newEntry);

		// Copy compressed data
		CopyData(sourceFile, newEntry);

		currentSpotD = ftell(outZip);
		currentSpotD = currentSpotD*60.0 / (double)expectedSize;
		currentSpot = (int)currentSpotD;
		while(p < currentSpot)
		{
			p++;
			Com_Printf("*");
		}
	}

	// Write the central directory
	dirEnd.centralDirOffset = (unsigned long)ftell(outZip);
	dirEnd.centralDirTotal = za->numZipFiles;

	for(i=0;i<za->numZipFiles;i++)
	{
		newEntry = za->zipFiles + i;
		WriteCentralEntry(outZip, za->zipFiles + i);
	}

	dirEnd.centralDirSize = (unsigned long)ftell(outZip) - dirEnd.centralDirOffset;

	WriteCentralDirEnd(outZip, &dirEnd);

	return 1;
}


int ZPATCH_Apply(FILE *patchFile, const char *tempName, const char *zipName)
{
	Com_Printf("\nNew game data is available for:\n%s\n\nApplying...\n", zipName);

	oldArchive.f = fopen(tempName, "rb");
	newArchive.f = patchFile;
	outZip = fopen(zipName, "wb");

	if(!oldArchive.f || !outZip)
	{
		fclose(patchFile);
		if(oldArchive.f)
			fclose(oldArchive.f);
		else
			Com_Printf("Couldn't open old archive: %s\n", tempName);
		if(outZip)
			fclose(outZip);
		else
			Com_Printf("Couldn't open output archive: %s\n", zipName);

		return 0;
	}

	Com_Printf("0%% ........................ 50%% ...................... 100%%\n");

	// Read expected size
	ReadLong(newArchive.f, &expectedSize);

	if(!LoadCentralDir(&oldArchive) || !ApplyPatch(&newArchive))
	{
		fclose(patchFile);
		fclose(oldArchive.f);
		fclose(outZip);
		return 0;
	}

	Com_Printf("\nUpdate completed.\n");

	fclose(patchFile);
	fclose(oldArchive.f);
	fclose(outZip);

	return 1;
}
